// Copyright Epic Games, Inc. All Rights Reserved.

#include "LyraGameplayCueManager.h"
#include "Engine/AssetManager.h"
#include "LyraLogChannels.h"
#include "GameplayCueSet.h"
#include "AbilitySystemGlobals.h"
#include "GameplayTagsManager.h"
#include "UObject/UObjectThreadContext.h"
#include "Async/Async.h"

#include UE_INLINE_GENERATED_CPP_BY_NAME(LyraGameplayCueManager)

//////////////////////////////////////////////////////////////////////


//NOTE: 目前来看就分为两类，一类是在打包后的注册加载，一类在编辑器中，分为两个小点情况：一种是预先全部加载，另一类是在运行时加载。

enum class ELyraEditorLoadMode
{
	// Loads all cues upfront; longer loading speed in the editor but short PIE times and effects never fail to play

	// 预先地导入所有GCue；在编辑器更长的导入速度但是PIE时间更短并且有效不会失败播放。
	LoadUpfront,

	// Outside of editor: Async loads as cue tag are registered
	// In editor: Async loads when cues are invoked
	//   Note: This can cause some 'why didn't I see the effect for X' issues in PIE and is good for iteration speed but otherwise bad for designers

	// 在编辑器外部环境：异步加载当GCue标签被注册。
	// 在编辑器：当GCue触发时异步加载。
	// NOTE: 这能造成一些像“为什么我看不到X效果”问题在PIE，并且是有好处的对于迭代速度，但是对于设计师不是友好的。
	PreloadAsCuesAreReferenced_GameOnly,

	// Async loads as cue tag are registered

	// 异步加载当GCue标签被注册。
	PreloadAsCuesAreReferenced
};

namespace LyraGameplayCueManagerCvars
{
	/**
	 * Autoregistering console command
	 */

	 /**
		* 自动注册控制台命令。
		*/
	static FAutoConsoleCommand CVarDumpGameplayCues(
		/**
		 * Lyra.DumpGameplayCues:
		 * 表示的是你想要在控制台注册的指令是什么，这就是你需要在控制台输入的指令，后面是应该携带的参数。
		 */
		TEXT("Lyra.DumpGameplayCues"),
		TEXT("Shows all assets that were loaded via LyraGameplayCueManager and are currently in memory."),
		/** Console command delegate type (with arguments.)  This is a void callback function that always takes a list of arguments. */

		/**
		 * FConsoleCommandWithArgsDelegate:
		 *
		 * 控制台命令代理类型(带着参数)。这是一个携带参数列表的无类型回调函数。
		 */
		FConsoleCommandWithArgsDelegate::CreateStatic(ULyraGameplayCueManager::DumpGameplayCues));

	static ELyraEditorLoadMode LoadMode = ELyraEditorLoadMode::LoadUpfront;
}

const bool bPreloadEvenInEditor = true;

//////////////////////////////////////////////////////////////////////
/**
 * Base class for asynchronous functions that are executed in the Task Graph system.
 */

 /**
	* FAsyncGraphTaskBase: 异步函数的基类，在任务图表系统中被执行。
	*/
struct FGameplayCueTagThreadSynchronizeGraphTask : public FAsyncGraphTaskBase
{
	TFunction<void()> TheTask;
	FGameplayCueTagThreadSynchronizeGraphTask(TFunction<void()>&& Task) : TheTask(MoveTemp(Task)) { }
	void DoTask(ENamedThreads::Type CurrentThread, const FGraphEventRef& MyCompletionGraphEvent) { TheTask(); }
	ENamedThreads::Type GetDesiredThread() { return ENamedThreads::GameThread; }
};

//////////////////////////////////////////////////////////////////////

ULyraGameplayCueManager::ULyraGameplayCueManager(const FObjectInitializer& ObjectInitializer)
	: Super(ObjectInitializer)
{
}

ULyraGameplayCueManager* ULyraGameplayCueManager::Get()
{
	/** Returns the gameplay cue manager singleton object, creating if necessary */

	/** GetGameplayCueManager: 返回GCue管理器单例对象，如果需要的话就创造。 */
	return Cast<ULyraGameplayCueManager>(UAbilitySystemGlobals::Get().GetGameplayCueManager());
}

void ULyraGameplayCueManager::OnCreated()
{
	Super::OnCreated();

	UpdateDelayLoadDelegateListeners();
}

void ULyraGameplayCueManager::LoadAlwaysLoadedCues()
{
	if (ShouldDelayLoadGameplayCues())
	{
		UGameplayTagsManager& TagManager = UGameplayTagsManager::Get();

		//@TODO: Try to collect these by filtering GameplayCue. tags out of native gameplay tags?

		//@TODO: 尝试去收集过滤后的GCue。超出原生游戏玩法标签的标签。

		/**
		 * NOTE: 目前推测，这个是存放额外的标签，因为什么呢，因为虚幻内置了GameplayCue顶级层，其他的GCue标签都在它这个顶级层
		 * 下面定义，可能有一些在外面的定义的标签，在这里加进去。
		 */
		TArray<FName> AdditionalAlwaysLoadedCueTags;

		for (const FName& CueTagName : AdditionalAlwaysLoadedCueTags)
		{
			/**
			 * Gets the FGameplayTag that corresponds to the TagName
			 *
			 * @param TagName The Name of the tag to search for
			 * @param ErrorIfNotfound: ensure() that tag exists.
			 *
			 * @return Will return the corresponding FGameplayTag or an empty one if not found.
			 */

			 /**
				* RequestGameplayTag:
				* 获取与标签名对应的GTag标签。
				*/
			FGameplayTag CueTag = TagManager.RequestGameplayTag(CueTagName, /*ErrorIfNotFound=*/ false);
			if (CueTag.IsValid())
			{
				ProcessTagToPreload(CueTag, nullptr);
			}
			else
			{
				UE_LOG(LogLyra, Warning, TEXT("ULyraGameplayCueManager::AdditionalAlwaysLoadedCueTags contains invalid tag %s"), *CueTagName.ToString());
			}
		}
	}
}

bool ULyraGameplayCueManager::ShouldAsyncLoadRuntimeObjectLibraries() const
{
	switch (LyraGameplayCueManagerCvars::LoadMode)
	{
	case ELyraEditorLoadMode::LoadUpfront:
		return true;
	case ELyraEditorLoadMode::PreloadAsCuesAreReferenced_GameOnly:
#if WITH_EDITOR
		if (GIsEditor)
		{
			return false;
		}
#endif
		break;
	case ELyraEditorLoadMode::PreloadAsCuesAreReferenced:
		break;
	}

	return !ShouldDelayLoadGameplayCues();
}

bool ULyraGameplayCueManager::ShouldSyncLoadMissingGameplayCues() const
{
	return false;
}

bool ULyraGameplayCueManager::ShouldAsyncLoadMissingGameplayCues() const
{
	return true;
}

/** 输出GCue信息。 */
void ULyraGameplayCueManager::DumpGameplayCues(const TArray<FString>& Args)
{
	/** Returns the gameplay cue manager singleton object, creating if necessary */

	/** GetGameplayCueManager: 返回GCue管理器的单例对象，如果需要将创造。 */
	ULyraGameplayCueManager* GCM = Cast<ULyraGameplayCueManager>(UAbilitySystemGlobals::Get().GetGameplayCueManager());
	if (!GCM)
	{
		UE_LOG(LogLyra, Error, TEXT("DumpGameplayCues failed. No ULyraGameplayCueManager found."));
		return;
	}

	const bool bIncludeRefs = Args.Contains(TEXT("Refs"));

	UE_LOG(LogLyra, Log, TEXT("=========== Dumping Always Loaded Gameplay Cue Notifies ==========="));
	for (UClass* CueClass : GCM->AlwaysLoadedCues)
	{
		/**
		 * Returns the path name of this object
		 * @param Object object to retrieve the path name for; NULL gives "None"
		 * @return path name of the object.
		 */

		/**
		 * GetPathNameSafe: 
		 * 返回对象的路径名。(安全路径名)
		 */
		UE_LOG(LogLyra, Log, TEXT("  %s"), *GetPathNameSafe(CueClass));
	}

	UE_LOG(LogLyra, Log, TEXT("=========== Dumping Preloaded Gameplay Cue Notifies ==========="));
	for (UClass* CueClass : GCM->PreloadedCues)
	{
		TSet<FObjectKey>* ReferencerSet = GCM->PreloadedCueReferencers.Find(CueClass);
		int32 NumRefs = ReferencerSet ? ReferencerSet->Num() : 0;
		UE_LOG(LogLyra, Log, TEXT("  %s (%d refs)"), *GetPathNameSafe(CueClass), NumRefs);
		if (bIncludeRefs && ReferencerSet)
		{
			for (const FObjectKey& Ref : *ReferencerSet)
			{
				/**
				 * Attempt to access the object from which this key was constructed.
				 * @return The object used to construct this key, or nullptr if it is no longer valid
				 */

				/**
				 * ResolveObjectPtr: 
				 * 尝试去访问此Key(FObjectKey)被构造所基于的对象。
				 */
				UObject* RefObject = Ref.ResolveObjectPtr();
				UE_LOG(LogLyra, Log, TEXT("    ^- %s"), *GetPathNameSafe(RefObject));
			}
		}
	}

	UE_LOG(LogLyra, Log, TEXT("=========== Dumping Gameplay Cue Notifies loaded on demand ==========="));
	int32 NumMissingCuesLoaded = 0;
	if (GCM->RuntimeGameplayCueObjectLibrary.CueSet)
	{
		for (const FGameplayCueNotifyData& CueData : GCM->RuntimeGameplayCueObjectLibrary.CueSet->GameplayCueData)
		{
			if (CueData.LoadedGameplayCueClass && !GCM->AlwaysLoadedCues.Contains(CueData.LoadedGameplayCueClass) && !GCM->PreloadedCues.Contains(CueData.LoadedGameplayCueClass))
			{
				NumMissingCuesLoaded++;
				UE_LOG(LogLyra, Log, TEXT("  %s"), *CueData.LoadedGameplayCueClass->GetPathName());
			}
		}
	}

	UE_LOG(LogLyra, Log, TEXT("=========== Gameplay Cue Notify summary ==========="));
	UE_LOG(LogLyra, Log, TEXT("  ... %d cues in always loaded list"), GCM->AlwaysLoadedCues.Num());
	UE_LOG(LogLyra, Log, TEXT("  ... %d cues in preloaded list"), GCM->PreloadedCues.Num());
	UE_LOG(LogLyra, Log, TEXT("  ... %d cues loaded on demand"), NumMissingCuesLoaded);
	UE_LOG(LogLyra, Log, TEXT("  ... %d cues in total"), GCM->AlwaysLoadedCues.Num() + GCM->PreloadedCues.Num() + NumMissingCuesLoaded);
}

void ULyraGameplayCueManager::OnGameplayTagLoaded(const FGameplayTag& Tag)
{
	FScopeLock ScopeLock(&LoadedGameplayTagsToProcessCS);
	bool bStartTask = LoadedGameplayTagsToProcess.Num() == 0;
	/** Structure that holds the current serialization state of UObjects */

	/** FUObjectSerializeContext: 处理当前UObject对象序列化状态的结构体。 */

	/**
	 * This a special version of singleton. It means that there is created only one instance for each thread.
	 * Calling Get() method is thread-safe.
	 */

	/**
	 * TThreadSingleton<T>:
	 * 这是一个具体的单例版本。它意味着每个线程仅创建一个实例。调用Get()方法是线程安全。
	 */

	 /** Gets the current serialization context */

	/** GetSerializeContext: 获取当前序列化上下文。 */
	FUObjectSerializeContext* LoadContext = FUObjectThreadContext::Get().GetSerializeContext();
	UObject* OwningObject = LoadContext ? LoadContext->SerializedObject : nullptr;
	/** 这里是设置GTag标签关联指定进程。 */

	/**
	 * Constructs a new item at the end of the array, possibly reallocating the whole array to fit.
	 *
	 * @param Args	The arguments to forward to the constructor of the new item.
	 * @return		Index to the new item
	 */

	/**
	 * Emplace: 
	 * 构造一个新的成员到数组的末尾，尽可能重新分配整个数组去匹配。
	 * NOTE: FLoadedGameplayTagToProcessData(const FGameplayTag& InTag, const TWeakObjectPtr<UObject>& InWeakOwner)
	 * 由此看到Emplace里的参数其实对应的是上面的构造函数的参数。
	 */
	LoadedGameplayTagsToProcess.Emplace(Tag, OwningObject);
	if (bStartTask)
	{
		TGraphTask<FGameplayCueTagThreadSynchronizeGraphTask>::CreateTask().ConstructAndDispatchWhenReady([]()
			{
				/** Whether execution is happening within MainLoop() */

				/** GIsRunning: 是否发生在MainLoop()函数内。(正在运行状态) */

				/**
				 * NOTE: 这是TSAN，TSAN(ThreadSanitizer)是一个由Google开发的线程竞争检测工具，集成于Clang和GCC编译器中，用于发现多线程程序中的数据竞争(Data Race)和死锁(Deadlock)问题。它通过运行时插桩技术，在不修改源代码的前提下，高效地检测并发错误。
				 * 在CoreGlobals.cpp内。
				 */
				if (GIsRunning)
				{
					if (ULyraGameplayCueManager* StrongThis = Get())
					{
						// If we are garbage collecting we cannot call StaticFindObject (or a few other static uobject functions), so we'll just wait until the GC is over and process the tags then

						// 如果我们在垃圾回收，我们不能调用StaticFindObject(或者其他静态对象函数)，如此我们将等GC处理完成并且此时处理Tags。
						if (IsGarbageCollecting())
						{
							StrongThis->bProcessLoadedTagsAfterGC = true;
						}
						else
						{
							StrongThis->ProcessLoadedTags();
						}
					}
				}
			});
	}
}

void ULyraGameplayCueManager::HandlePostGarbageCollect()
{
	if (bProcessLoadedTagsAfterGC)
	{
		ProcessLoadedTags();
	}
	bProcessLoadedTagsAfterGC = false;
}

void ULyraGameplayCueManager::ProcessLoadedTags()
{
	TArray<FLoadedGameplayTagToProcessData> TaskLoadedGameplayTagsToProcess;
	// 这里说明一下，这里是代码执行出花括号就解锁了。
	{
		// Lock LoadedGameplayTagsToProcess just long enough to make a copy and clear

		// 锁LoadedGameplayTagsToProcess仅仅是确保足够的去复制和清空。
		FScopeLock TaskScopeLock(&LoadedGameplayTagsToProcessCS);
		TaskLoadedGameplayTagsToProcess = LoadedGameplayTagsToProcess;
		LoadedGameplayTagsToProcess.Empty();
	}

	// This might return during shutdown, and we don't want to proceed if that is the case

	// 这可能是在关机期间返回，并且我们不想去处理如果是这种情况的话。
	if (GIsRunning)
	{
		if (RuntimeGameplayCueObjectLibrary.CueSet)
		{
			for (const FLoadedGameplayTagToProcessData& LoadedTagData : TaskLoadedGameplayTagsToProcess)
			{
				if (RuntimeGameplayCueObjectLibrary.CueSet->GameplayCueDataMap.Contains(LoadedTagData.Tag))
				{
					/**
					 * Slightly different than !IsValid(), returns true if this used to point to a UObject, but doesn't any more and has not been assigned or reset in the mean time.
					 * @param bIncludingIfPendingKill if this is true, pendingkill objects are considered stale
					 * @param bThreadsafeTest set it to true when testing outside of Game Thread. Results in false if WeakObjPtr point to an existing object (no flags checked)
					 * @return true if this used to point at a real object but no longer does.
					 */

					/**
					 * IsStale: 
					 * 比IsValid()稍微有点不同，如果指针曾指向一个UObect对象，但现在不在指向且在此期间未被赋值或重置，则返回True。
					 * NOTE: IsStale()用于判断弱指针对象是否被垃圾回收，如果被垃圾回收，则返回True，否则是False。
					 */
					if (!LoadedTagData.WeakOwner.IsStale())
					{
						ProcessTagToPreload(LoadedTagData.Tag, LoadedTagData.WeakOwner.Get());
					}
				}
			}
		}
		else
		{
			UE_LOG(LogLyra, Warning, TEXT("ULyraGameplayCueManager::OnGameplayTagLoaded processed loaded tag(s) but RuntimeGameplayCueObjectLibrary.CueSet was null. Skipping processing."));
		}
	}
}

void ULyraGameplayCueManager::ProcessTagToPreload(const FGameplayTag& Tag, UObject* OwningObject)
{
	/**
	 * 在编辑器环境下的加载无法操作，只能去处理预加载。
	 */
	switch (LyraGameplayCueManagerCvars::LoadMode)
	{
	case ELyraEditorLoadMode::LoadUpfront:  // UE默认应该就是预先导入所有GCue。
		return;
	case ELyraEditorLoadMode::PreloadAsCuesAreReferenced_GameOnly:
#if WITH_EDITOR
		/**
		 *	True if we are in the editor.
		 *	Note that this is still true when using Play In Editor. You may want to use GWorld->HasBegunPlay in that case.
		 */

		/**
		 * GIsEditor:
		 * 如果我们在编辑器里，则返回True。
		 * 注意，当我们使用PIE时，仍然返回True。在这种情况下，你也许可能会使用GWorld->HasBegunPlay。
		 */
		if (GIsEditor)
		{
			return;
		}
#endif
		break;
	case ELyraEditorLoadMode::PreloadAsCuesAreReferenced:
		break;
	}

	// 这下面就是进行异步加载的注册过程。
	/** An ObjectLibrary for the GameplayCue Notifies. Wraps 2 underlying UObjectLibraries plus options/delegates for how they are loaded */

	/**
	 * FGameplayCueObjectLibrary:
	 * 用于GCNotify的对象库。封装了2个底层的对象库Plus/代理为了如何去加载。
	 */
	 /** Set to put the loaded asset data into. If null we will use the global set (RuntimeGameplayCueObjectLibrary.CueSet) */

	/** CueSet: 设置去存放加载的资源数据位置。如果为空，我们将使用全局集合(RuntimeGameplayCueObjectLibrary.CueSet)。 */
	check(RuntimeGameplayCueObjectLibrary.CueSet);
	/** Maps GameplayCue Tag to index into above GameplayCues array. */

	/** GameplayCueDataMap: 映射GCTag到关于的GCue数组的索引位置。 */

	/**
	 * 在UGameplayCueSet中，有两个属性GameplayCueData和GameplayCueDataMap，其中GameplayCueData是存储FGameplayCueNotifyData数据的数组，
	 * GameplayCueDataMap是存储GTag标签到GameplayCueData数组的索引的映射。
	 * TArray<FGameplayCueNotifyData> GameplayCueData;
	 * TMap<FGameplayTag, int32> GameplayCueDataMap;
	 */
	int32* DataIdx = RuntimeGameplayCueObjectLibrary.CueSet->GameplayCueDataMap.Find(Tag);
	if (DataIdx && RuntimeGameplayCueObjectLibrary.CueSet->GameplayCueData.IsValidIndex(*DataIdx))
	{
		const FGameplayCueNotifyData& CueData = RuntimeGameplayCueObjectLibrary.CueSet->GameplayCueData[*DataIdx];
		/**
		 * Find an optional object.
		 * @see StaticFindObject()
		 */
		/**
		 * FindObject: 寻找一个可操作的对象。
		 */
		//UPROPERTY(EditAnywhere, Category = GameplayCue, meta = (AllowedClasses = "/Script/GameplayAbilities.GameplayCueNotify_Static, /Script/GameplayAbilities.GameplayCueNotify_Actor"))
		//D:\MakeFiles\ProgramFiles\UE\UE_5.2\Engine\Plugins\Runtime\GameplayAbilities\Source\GameplayAbilities\Private\GameplayCueNotify_Static.cpp => /Script/GameplayAbilities.GameplayCueNotify_Static
		//FSoftObjectPath GameplayCueNotifyObj;
		// AllowedClasses
		/// [PropertyMetadata] Used for FSoftObjectPath, ComponentReference and UClass properties.  Comma delimited list that indicates the class type(s) of assets to be displayed in the asset picker(FSoftObjectPath) or component picker or class viewer (UClass).
		/// 用于FSoftObjectPath，ComponentReference和UClass属性。逗号列表，明确被展示在(FSoftObjectPath)选择器，组件选择器和(UClass)展示区的资产的类型。
		UClass* LoadedGameplayCueClass = FindObject<UClass>(nullptr, *CueData.GameplayCueNotifyObj.ToString());
		if (LoadedGameplayCueClass)
		{
			RegisterPreloadedCue(LoadedGameplayCueClass, OwningObject);
		}
		else
		{
			bool bAlwaysLoadedCue = OwningObject == nullptr;
			TWeakObjectPtr<UObject> WeakOwner = OwningObject;
			/**
			 * This is the primary streamable operation. Requests streaming of one or more target objects. When complete, a delegate function is called. Returns a Streamable Handle.
			 *
			 * @param TargetsToStream		Assets to load off disk
			 * @param DelegateToCall		Delegate to call when load finishes. Will be called on the next tick if asset is already loaded, or many seconds later
			 * @param Priority				Priority to pass to the streaming system, higher priority will be loaded first
			 * @param bManageActiveHandle	If true, the manager will keep the streamable handle active until explicitly released
			 * @param bStartStalled			If true, the handle will start in a stalled state and will not attempt to actually async load until StartStalledHandle is called on it
			 * @param DebugName				Name of this handle, will be reported in debug tools
			 */

			 /**
				* RequestAsyncLoad:
				* 这是主要的可流式操作。一个或多个节点的请求流。当完成时，一个代理函数被调用。返回可流式句柄。
				*/
			StreamableManager.RequestAsyncLoad(CueData.GameplayCueNotifyObj, FStreamableDelegate::CreateUObject(this, &ThisClass::OnPreloadCueComplete, CueData.GameplayCueNotifyObj, WeakOwner, bAlwaysLoadedCue), FStreamableManager::DefaultAsyncLoadPriority, false, false, TEXT("GameplayCueManager"));
		}
	}
}
/**
 * A struct that contains a string reference to an object, either a package, a top level asset or a subobject.
 * This can be used to make soft references to assets that are loaded on demand.
 * This is stored internally as an FTopLevelAssetPath pointing to the top level asset (/package/path.assetname) and an optional string subobject path.
 * If the MetaClass metadata is applied to a FProperty with this the UI will restrict to that type of asset.
 */

/**
 * FSoftObjectPath: 
 * 包含一个对象的字符串引用的结构体，要么是包，顶级资产要么是子类对象。
 * 这能够被用于制造再被需要时被加载的资产的引用。
 * 这是内部存储作为顶级资产(/package/path.assetname)的FTopLevelAssetPath指针并且可操作的字符串子类对象路径。
 * 如果这个元类的元数据被应用到FProperty属性，在UI时将会限制资产类型。
 */
void ULyraGameplayCueManager::OnPreloadCueComplete(FSoftObjectPath Path, TWeakObjectPtr<UObject> OwningObject, bool bAlwaysLoadedCue)
{
	if (bAlwaysLoadedCue || OwningObject.IsValid())
	{
		/**
		 * Attempts to find a currently loaded object that matches this path
		 *
		 * @return Found UObject, or nullptr if not currently in memory
		 */

		/**
		 * ResolveObject: 尝试去寻找与这个路径匹配的加载对象。
		 */
		if (UClass* LoadedGameplayCueClass = Cast<UClass>(Path.ResolveObject()))
		{
			RegisterPreloadedCue(LoadedGameplayCueClass, OwningObject.Get());
		}
	}
}

void ULyraGameplayCueManager::RegisterPreloadedCue(UClass* LoadedGameplayCueClass, UObject* OwningObject)
{
	check(LoadedGameplayCueClass);

	const bool bAlwaysLoadedCue = OwningObject == nullptr;
	/**
	 * 如果GCue没有所有者对象，就认为是永久加载的。
	 */
	if (bAlwaysLoadedCue)
	{

		/** 存储永久加载的提示类，这些类不会被自动卸载。 */
		AlwaysLoadedCues.Add(LoadedGameplayCueClass);

		/** 存储通过引用计数管理的提示类。 */
		PreloadedCues.Remove(LoadedGameplayCueClass);

		/** 用于跟踪每个提示类的引用者，实现引用计数卸载机制。 */
		PreloadedCueReferencers.Remove(LoadedGameplayCueClass);
	}

	/**
	 * 这里是判断所有者对象不是GCue本身，所有者对象对象不是GCue的默认对象，也不是被标记为永久的GCue。
	 */
	else if ((OwningObject != LoadedGameplayCueClass) && (OwningObject != LoadedGameplayCueClass->GetDefaultObject()) && !AlwaysLoadedCues.Contains(LoadedGameplayCueClass))
	{
		PreloadedCues.Add(LoadedGameplayCueClass);
		/**
		 * Find the value associated with a specified key, or if none exists,
		 * adds a value using the default constructor.
		 *
		 * @param Key The key to search for.
		 * @return A reference to the value associated with the specified key.
		 */

		/**
		 * FindOrAdd: 
		 * 寻找关联具体Key的值，如果不存在，添加一个值用于默认构造。
		 */
		TSet<FObjectKey>& ReferencerSet = PreloadedCueReferencers.FindOrAdd(LoadedGameplayCueClass);
		ReferencerSet.Add(OwningObject);
	}
}

void ULyraGameplayCueManager::HandlePostLoadMap(UWorld* NewWorld)
{

	/**
	 * NOTE: 目前推测是在切换关卡时释放上个关卡的GCue，让其进行垃圾回收。
	 */
	if (RuntimeGameplayCueObjectLibrary.CueSet)
	{
		for (UClass* CueClass : AlwaysLoadedCues)
		{
			/** Nulls reference to the loaded class. Note this doesn't remove the entire cue from the internal data structure, just the hard ref to the loaded class */

			/** 将已经导入的类置为空引用。注意，这不会从内部数据结构体中移除导入的GCue，仅仅是处理导入的类的强引用。 */

			/** TObjectPtr指向的对象的引用失效或者是指向的引用是nullptr时，TObjectPtr指向的对象就可以被垃圾回收系统清理。 */
			RuntimeGameplayCueObjectLibrary.CueSet->RemoveLoadedClass(CueClass);
		}

		for (UClass* CueClass : PreloadedCues)
		{
			RuntimeGameplayCueObjectLibrary.CueSet->RemoveLoadedClass(CueClass);
		}
	}

	for (auto CueIt = PreloadedCues.CreateIterator(); CueIt; ++CueIt)
	{
		TSet<FObjectKey>& ReferencerSet = PreloadedCueReferencers.FindChecked(*CueIt);
		for (auto RefIt = ReferencerSet.CreateIterator(); RefIt; ++RefIt)
		{
			/**
			 * Attempt to access the object from which this key was constructed.
			 * @return The object used to construct this key, or nullptr if it is no longer valid
			 */

			/**
			 * ResolveObjectPtr: 尝试去访问构造此键的对象。
			 * NOTE: 就是Set对应的这个元素。
			 */
			if (!RefIt->ResolveObjectPtr())
			{
				/** RemoveCurrent: 从Set中移除当前元素。 */
				RefIt.RemoveCurrent();
			}
		}
		if (ReferencerSet.Num() == 0)
		{
			PreloadedCueReferencers.Remove(*CueIt);
			CueIt.RemoveCurrent();
		}
	}
}

void ULyraGameplayCueManager::UpdateDelayLoadDelegateListeners()
{
	UGameplayTagsManager::Get().OnGameplayTagLoadedDelegate.RemoveAll(this);
	/** Called after garbage collection */

	/** GetPostGarbageCollect: 在垃圾回收之后调用。 */
	FCoreUObjectDelegates::GetPostGarbageCollect().RemoveAll(this);
	/** Sent at the end of LoadMap */

	/** PostLoadMapWithWorld: 在导入地图结束之后调用。 */
	FCoreUObjectDelegates::PostLoadMapWithWorld.RemoveAll(this);

	switch (LyraGameplayCueManagerCvars::LoadMode)
	{
	case ELyraEditorLoadMode::LoadUpfront:
		return;
	case ELyraEditorLoadMode::PreloadAsCuesAreReferenced_GameOnly:
#if WITH_EDITOR
		if (GIsEditor)
		{
			return;
		}
#endif
		break;
	case ELyraEditorLoadMode::PreloadAsCuesAreReferenced:
		break;
	}

	UGameplayTagsManager::Get().OnGameplayTagLoadedDelegate.AddUObject(this, &ThisClass::OnGameplayTagLoaded);
	FCoreUObjectDelegates::GetPostGarbageCollect().AddUObject(this, &ThisClass::HandlePostGarbageCollect);

	/** Sent at the end of LoadMap */

	/**
	 * FCoreUObjectDelegates::FPostLoadMapDelegate FCoreUObjectDelegates::PostLoadMapWithWorld;
	 * 
	 * 当导入地图结束时调用代理。
	 */
	FCoreUObjectDelegates::PostLoadMapWithWorld.AddUObject(this, &ThisClass::HandlePostLoadMap);
}

bool ULyraGameplayCueManager::ShouldDelayLoadGameplayCues() const
{
	/**
	 * 当是客户端延迟加载并且不是专用服务器的服务端时，返回True。
	 */
	const bool bClientDelayLoadGameplayCues = true;
	return !IsRunningDedicatedServer() && bClientDelayLoadGameplayCues;
}
/**
 * A primary asset type, represented as an FName internally and implicitly convertible back and forth
 * This exists so the blueprint API can understand it's not a normal FName
 */

/**
 * FPrimaryAssetType: 
 * 一个原始资产类型，内部是作为FName并且隐式地来回转换。
 * 这是存在的，蓝图API能够理解它不是正常的FName。
 */
const FPrimaryAssetType UFortAssetManager_GameplayCueRefsType = TEXT("GameplayCueRefs");
const FName UFortAssetManager_GameplayCueRefsName = TEXT("GameplayCueReferences");
const FName UFortAssetManager_LoadStateClient = FName(TEXT("Client"));

void ULyraGameplayCueManager::RefreshGameplayCuePrimaryAsset()
{
	TArray<FSoftObjectPath> CuePaths;
	/** Returns the Runtime cueset, which is the global cueset used in runtime, as opposed to the editor cue set which is used only when running the editor */

	/**
	 * GetRuntimeCueSet:
	 * 返回运行的GCue集合，这是在运行期间被用在全局的GCue集合，区别于仅在运行编辑器时使用的编辑器提示集。
	 */
	UGameplayCueSet* RuntimeGameplayCueSet = GetRuntimeCueSet();
	if (RuntimeGameplayCueSet)
	{
		RuntimeGameplayCueSet->GetSoftObjectPaths(CuePaths);
	}

	/** A struct with a list of asset bundle entries. If one of these is inside a UObject it will get automatically exported as the asset registry tag AssetBundleData */

	/** FAssetBundleData: 一个资产包条目列表的结构体。如果这些对象的其中之一是UObject，它将会自动作为资产注册标签AssetBundleData被暴露。 */
	FAssetBundleData BundleData;
	/** Adds multiple assets at once, truncating to top-level asset paths if necessary */

	/** AddBundleAssetsTruncated: 在一次添加多种资产，如果需要，截断为顶级资产路径。 */
	BundleData.AddBundleAssetsTruncated(UFortAssetManager_LoadStateClient, CuePaths);

	/**
	 * This identifies an object as a "primary" asset that can be searched for by the AssetManager and used in various tools
	 */

	/**
	 * FPrimaryAssetId: 
	 * 这是一个对象作为原始资产的认证，能够被AssetManager搜索并且勇于这种工具当中。
	 */
	FPrimaryAssetId PrimaryAssetId = FPrimaryAssetId(UFortAssetManager_GameplayCueRefsType, UFortAssetManager_GameplayCueRefsName);
	/**
	 * Adds or updates a Dynamic asset, which is a runtime-specified asset that has no on disk representation, so has no FAssetData. But it can have bundle state and a path.
	 *
	 * @param FPrimaryAssetId	Type/Name of the asset. The type info will be created if it doesn't already exist
	 * @param AssetPath			Path to the object representing this asset, this is optional for dynamic assets
	 * @param BundleData		List of Name->asset paths that represent the possible bundle states for this asset
	 * @return					True if added
	 */

	/**
	 * AddDynamicAsset: 
	 * 添加或更新动态资产，是在运行时的具体资产没有在磁盘存在的，因此没有FAssetData。但是能够被绑定状态和路径。
	 */
	UAssetManager::Get().AddDynamicAsset(PrimaryAssetId, FSoftObjectPath(), BundleData);
}

